# 原生koa2实现jsonp
30 秒速记
JSONP= 利用<script>不受同源策略限制,服务端返回cb({...})这段JS,浏览器执行时把数据交给前端定义好的cb- 只能
GET,不能带自定义请求头,出错了前端也拿不到状态码 callback名必须校验,比如/^[\w$.]{1,64}$/,原样拼进响应就是XSS- 浏览器会带上目标站的
Cookie,敏感数据走JSONP等于让任何网站都能读 - 新项目用
CORS或同源代理,JSONP只留给老页面兼容
JSONP 说白了就是借 <script> 标签能跨域加载脚本的特性,让服务端把数据包成一段函数调用返回。 前端先在全局定义好一个函数,比如 handleData,再插一个 <script src="/api?callback=handleData">;服务端返回 handleData({"ok":true}),浏览器一执行,数据就进了这个函数。在 Koa2 里原生实现只要几行:读 ctx.query.callback,JSON.stringify 数据,拼起来,ctx.type 设成 text/javascript。它的硬伤是只能 GET、错误处理很弱,而且回调名不校验就是注入点,所以现在只在兼容老页面时用。
理解「原生koa2实现jsonp」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 middleware 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
JSONP 的完整链路可以拆成前后端两半来看。
前端要做三件事:注册一个全局回调、插入 <script>、收尾清理。
function jsonp(url, timeout = 5000) {
return new Promise((resolve, reject) => {
const cbName = 'jsonp_' + Date.now() + '_' + Math.random().toString(36).slice(2)
const script = document.createElement('script')
const timer = setTimeout(() => cleanup(reject, new Error('timeout')), timeout)
function cleanup(fn, value) {
clearTimeout(timer)
delete window[cbName]
script.remove()
fn(value)
}
window[cbName] = data => cleanup(resolve, data) // 服务端返回的脚本会调用它
script.onerror = () => cleanup(reject, new Error('load error'))
script.src = `${url}?callback=${cbName}`
document.body.appendChild(script)
})
}
jsonp('http://localhost:3000/getData.jsonp').then(console.log)
// => { success: true, data: { text: 'this is a jsonp api', time: ... } }
每次请求用一个唯一的回调名,并发多个请求也不会串数据;成功、失败、超时都要删掉全局函数和 script 节点。
后端要做的是把数据包成一段合法的 JS,并守住回调名:
const CALLBACK_RE = /^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)*$/
app.use(async (ctx, next) => {
if (ctx.method !== 'GET' || ctx.path !== '/getData.jsonp') return next()
const cb = ctx.query.callback || 'callback'
if (cb.length > 64 || !CALLBACK_RE.test(cb)) {
ctx.status = 400
ctx.body = 'invalid callback'
return
}
ctx.type = 'text/javascript'
ctx.set('X-Content-Type-Options', 'nosniff')
ctx.body = `/**/ ;${cb}(${JSON.stringify({ success: true })})`
})
开头的 ; 是为了防止前面有别的脚本没写分号,拼起来变成函数调用;/**/ 是历史上防御 Rosetta Flash 攻击留下的习惯写法。跨域能力来自 <script> 标签本身,text/javascript 只是让浏览器认它是脚本;配上 nosniff 之后,类型不对的响应浏览器会直接拒绝执行。
面试官追问
追问 1JSONP 为什么能跨域,fetch 却不行?
同源策略限制的是脚本读取跨域响应,而 <script>、<img> 这类标签本来就允许加载跨域资源。JSONP 不是去「读」响应,而是让响应自己作为脚本执行,把数据塞给你提前定义的函数。
追问 2请求 ?callback=alert(document.cookie)//,服务端原样拼接会怎样?
响应变成 alert(document.cookie)//({...}),这就是一段任意脚本,在调用方页面里执行。回调名必须用白名单正则校验,比如 /^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)*$/,不合法直接返回 400。
追问 3用户资料接口能不能做成 JSONP?
不能。<script> 请求会自动带上目标站的 Cookie,恶意网站插一个 <script src="你的接口?callback=steal">,就能拿到当前登录用户的资料。这叫 JSONP 劫持,带身份的数据只能走配好白名单的 CORS。
追问 4JSONP 请求失败了,前端怎么知道?
拿不到状态码,只能监听 script.onerror,再加一个超时定时器兜底,比如 5s 还没回调就当失败。服务端返回 500 时 onerror 会触发,但具体错误信息是拿不到的。
追问 5新接口还要不要支持 JSONP?
没有老页面依赖就不加。CORS 支持所有方法、能拿状态码、数据不会被当代码执行,配好 Access-Control-Allow-Origin 白名单就行。
# 原生koa2实现jsonp
# 前言
30 秒速记
- 前端跨域拿数据,服务端得提供跨域接口,老项目里最常见的做法就是
JSONP - 原理:
<script>能跨域加载,服务端返回cb(data),浏览器执行时把数据交给cb - 局限:只有
GET、拿不到状态码、回调名要防注入、带Cookie的敏感数据不能走 - 现在的首选是
CORS;调用方是服务端或App时根本不存在跨域问题,直接给JSON
跨域取数据这件事,早些年最常见的解法就是 JSONP,这一章要讲的就是在 Koa2 里怎么提供这种接口。 浏览器的同源策略不让 XHR 读别的域的响应,但 <script> 标签可以随便加载跨域脚本,JSONP 就钻了这个空子:让服务端返回一段调用前端函数的 JS。它在 IE8、IE9 不支持完整 CORS 的年代很实用,现在浏览器都支持 CORS 了,新接口我基本不会再提供 JSONP,只有要兼容老页面时才保留。
在项目复杂的业务场景,有时候需要在前端跨域获取数据,这时候提供数据的服务就需要提供跨域请求的接口,通常是使用JSONP的方式提供跨域接口。
理解「前言」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1把普通 JSON 接口的响应类型改成 text/javascript,算不算实现了 JSONP?
不算。浏览器执行 {"price":99} 这段脚本什么也不会发生,数据进不了你的代码。关键是服务端要把数据包成 render({"price":99}),并且页面提前定义好 render。
追问 2移动端 App 也要调这个接口,需要做成 JSONP 吗?
不需要。跨域是浏览器的限制,App 原生请求、服务端请求都没有同源策略,直接返回 JSON 最好。JSONP 只该给依赖 <script> 加载的老网页用。
追问 3现在新写一个跨域接口,你会选 JSONP 还是 CORS?
CORS。支持 POST、能带自定义头、前端能拿到状态码,数据也不会被当成代码执行。服务端配好 Access-Control-Allow-Origin 白名单就行,Koa 里用 @koa/cors 一行搞定。
